可以實作的檔案類型包括「TXT、CSV、JSON、XML、XLS」,每一種檔案格式都有對應的IO函式庫。
在C#,可以使用Image函式庫來處理圖片,或者透過Stream 讀取Byte[]後轉成陣列
程式就像國文學科一樣,每個人都能寫出一篇作文,但是若要寫出一篇文筆簡潔的文章,需要對國文的修辭、文法、標點符號、成語、詞彙有一定的掌握。
同樣的,要對程式碼重構,對於「資料結構」和「物件導向」需要有非常高的掌握度,對於初學者來說並不容易
這次重構要處理的問題:
「自我介紹」程式經過七天的最終程式碼放在Github上,有需要可以自行下載取用
https://github.com/ted59438/2019ITHome

先看重構後的程式碼:
https://github.com/ted59438/2019ITHome/blob/master/IT_Day01/View/IntroductionForm.cs
在看重構前的程式碼:
https://github.com/ted59438/2019ITHome/blob/master/IT_Day01/View/IntroductionForm_before7Days.cs
我們來看看之前的程式碼,首先看到「按下自我介紹 (showIntroductionBtn_Click)」的事件程式碼,再看看 「按下保存個人資訊 (saveBtn_Click)」的事件程式碼。
// IntroductionForm.cs
    TextBox[] allTextBox = new TextBox[] { nameTextBox, homeTownTextBox, birthdate_YearBox, birthdate_MonthBox, birthdate_DayBox };
    string[] allTextBoxName = new string[]{LanguageResources.Name, LanguageResources.HomeTown,
                                           LanguageResources.Birthday_Year, LanguageResources.Birthday_Month, LanguageResources.Birthday_Day };
    StringBuilder errorMsg = new StringBuilder();
    for (int i = 0; i < allTextBox.GetLength(0); i++)
    {
        if (string.IsNullOrEmpty(allTextBox[i].Text))
        {
            errorMsg.AppendLine(string.Format(LanguageResources.Message_PleaseInput, allTextBoxName[i]));
        }
    }
    if (errorMsg.ToString() != "")
    {
        MessageBox.Show(errorMsg.ToString(), "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return;
    }
    // 校驗日期格式是否正確
    if (!Regex.IsMatch(birthdate_YearBox.Text, @"\d") || !Regex.IsMatch(birthdate_MonthBox.Text, @"\d") || !Regex.IsMatch(birthdate_DayBox.Text, @"\d"))
    {
        MessageBox.Show(LanguageResources.Message_BirthdayNeedNum, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return;
    }
這段程式碼被拆成兩個函式方法
checkAllColumnIsNotEmpty():校驗所有欄位是不是都有輸入
checkDateIsValidate(): 校驗日期格式是否正確
// IntroductionForm.cs
private void checkAllColumnIsNotEmpty()
{
    // 校驗每個欄位是否輸入
    TextBox[] allTextBox = new TextBox[] { nameTextBox, homeTownTextBox, birthdate_YearBox, birthdate_MonthBox, birthdate_DayBox };
    string[] allTextBoxName = new string[]{LanguageResources.Name, LanguageResources.HomeTown,
                                           LanguageResources.Birthday_Year, LanguageResources.Birthday_Month, LanguageResources.Birthday_Day };
    StringBuilder errorMsg = new StringBuilder();
    for (int i = 0; i < allTextBox.GetLength(0); i++)
    {
        if (string.IsNullOrEmpty(allTextBox[i].Text))
        {
            errorMsg.AppendLine(string.Format(LanguageResources.Message_PleaseInput, allTextBoxName[i]));
        }
    }
    if (photoBox.Image == null)
    {
        errorMsg.AppendLine(LanguageResources.Message_NoImage);
    }
    if (errorMsg.ToString() != "")
    {
        throw new Exception(errorMsg.ToString());
    }
}
/// <summary>
/// 校驗日期格式是否正確
/// </summary>
private void checkDateIsValidate()
{
    if (!Regex.IsMatch(birthdate_YearBox.Text, @"\d") || !Regex.IsMatch(birthdate_MonthBox.Text, @"\d") || !Regex.IsMatch(birthdate_DayBox.Text, @"\d"))
    {
        throw new Exception(LanguageResources.Message_BirthdayNeedNum);
    }
}
畫面取得「姓名」、「家鄉」、「出生年月日」的程式碼也重複了
// IntroductionForm.cs
private void showIntroductionBtn_Click(object sender, EventArgs e)
{        
    // ... 前後程式碼省略
    string name = nameTextBox.Text;
    string homeTown = homeTownTextBox.Text;
    int today_Year = DateTime.Today.Year;
    int today_Month = DateTime.Today.Month;
    int today_Day = DateTime.Today.Day;
    
    // ... 前後程式碼省略
}
// IntroductionForm.cs
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
    // ... 前後程式碼省略
    string name = nameTextBox.Text;
    string homeTown = homeTownTextBox.Text;
    int birthDate_Year = int.Parse(birthdate_YearBox.Text);
    int birthDate_Month = int.Parse(birthdate_MonthBox.Text);
    int birthDate_Day = int.Parse(birthdate_DayBox.Text);
    
    // ... 前後程式碼省略
}
建立一個新的C#類別檔案,取名為「IntroductionOBJ.cs」,把自我介紹的「姓名」、「家鄉」、「出生日期」、「相片」,用一個物件類別「IntroductionOBJ」封裝
// IntroductionOBJ.cs
using System;
namespace IT_Day01
{
    public class IntroductionOBJ
    {
        /// <summary>
        /// 姓名
        /// </summary>
        public string name { get; set; }
        /// <summary>
        /// 家鄉
        /// </summary>
        public string homeTown { get; set; }
        /// <summary>
        /// 生日
        /// </summary>
        public DateTime birthDate { get; set; }
        /// <summary>
        /// 相片
        /// </summary>
        public byte[] photo { get; set; }
    }
}
在 自我介紹主視窗 (IntroductionForm.cs) 撰寫一個方法 (getIntroductionFromView()),每一次從畫面所有的欄位資料取得之後,以IntroductionOBJ 物件 存放
// IntroductionForm.cs
/// <summary>
/// 從畫面上的所有欄位取得自我介紹資訊
/// </summary>
/// <returns></returns>
private IntroductionOBJ getIntroductionFromView()
{
    try
    {
        checkAllColumnIsNotEmpty();
        checkDateIsValidate();
        IntroductionOBJ introductionOBJ = new IntroductionOBJ();
        introductionOBJ.name = nameTextBox.Text;
        introductionOBJ.homeTown = homeTownTextBox.Text;
        introductionOBJ.birthDate = new DateTime(int.Parse(birthdate_YearBox.Text), int.Parse(birthdate_MonthBox.Text), int.Parse(birthdate_DayBox.Text));
        introductionOBJ.photo = PhotoHelper.ImageToBytes(photoBox.Image);
        return introductionOBJ;
    }
    catch (Exception error)
    {
        throw new Exception(error.Message);
    }
}
到目前重構完getIntroductionFromView()後的「按下自我介紹」跟「保存個人資訊」Click 事件的程式碼:
// 按下自我介紹
private void showIntroductionBtn_Click(object sender, EventArgs e)
{
    IntroductionOBJ introductionOBJ = getIntroductionFromView();
    
    // 計算年齡
    int yearOld;
    DateTime todayDate = DateTime.Today;
    
    yearOld = today_Year - introductionOBJ.birthDate.Year;
    if (todayDate.Month < introductionOBJ.birthDate.Month || (todayDate.Month == introductionOBJ.birthDate.Month && todayDate.Day < introductionOBJ.birthDate.Day))
    {
        yearOld = yearOld - 1;
    }
    string introductionText = string.Format(LanguageResources.Message_IntrouductionText, introductionOBJ.name, introductionOBJ.homeTown, yearOld);
    MessageBox.Show(introductionText);
}
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
    try
    {
        IntroductionOBJ introductionOBJ = getIntroductionFromView();
        
        // ... 省略Json寫檔的程式碼
        JObject introductionJson = new JObject();
        introductionJson.Add("Name", introductionOBJ.name);
        introductionJson.Add("HomeTown", introductionOBJ.homeTown);
        introductionJson.Add("BirthDate", 
        string.Format("{0}-{1}-{2}", introductionOBJ.birthDate.Year,                                                          
                                     introductionOBJ.birthDate.Month,                                                           
                                     introductionOBJ.birthDate.Day));
        string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
        string jsonPath = Path.Combine(dirPath, "Introduction.json");
        string imagePath = Path.Combine(dirPath, "Photo.jpeg");
        // 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);
        }
        // 檔案不存在,產生個人資訊的檔案
        if (!File.Exists(jsonPath))
        {
            File.Create(jsonPath).Close();
        }
        // 保存個人資訊到JSON
        File.WriteAllText(jsonPath, JsonConvert.SerializeObject(introductionJson));
        // 保存個人大頭貼到Jpeg 圖片
        photoBox.Image.Save(imagePath, ImageFormat.Jpeg);
    }
    catch (Exception error)
    {
        MessageBox.Show(error.Message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}
新增專門負責讀寫檔的類別「FileHelper.cs」讓 WinForm 的「UI 邏輯」和 「JSON的讀寫操作」彼此不會互相牽扯。
// FileHelper.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text.RegularExpressions;
namespace IT_Day01
{
    public class FileHelper
    {
        private static string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
        private static string jsonPath = Path.Combine(dirPath, "Introduction.json");
        private static string imagePath = Path.Combine(dirPath, "Photo.jpeg");
        /// <summary>
        /// 自我介紹 JSON讀檔流程
        /// </summary>
        /// <returns></returns>
        public static IntroductionOBJ processRead()
        {
            IntroductionOBJ introductionOBJ;
            JObject introductionJson;
            if (!prepareRead())
            {
                return new IntroductionOBJ();
            }
            introductionJson = readFromJson();
            if (!checkJsonIsVailed(introductionJson))
            {
                return new IntroductionOBJ();
            }
            introductionOBJ = getIntroductionJsonStr(introductionJson);
            introductionOBJ.photo = PhotoHelper.readImageStreamFromFile(imagePath).ToArray() ;
            return introductionOBJ;
        }
        /// <summary>
        /// 讀檔前的檢查
        /// </summary>
        /// <returns></returns>
        private static bool prepareRead()
        {
            if (!Directory.Exists(dirPath) || !File.Exists(jsonPath) || !File.Exists(imagePath))
            {
                return false;
            }
            return true;
        }
        /// <summary>
        /// 讀取JSON文字
        /// </summary>
        /// <returns></returns>
        private static JObject readFromJson()
        {
            string introductionJsonStr = File.ReadAllText(jsonPath);
            JObject introductionJson = (JObject)JsonConvert.DeserializeObject(introductionJsonStr);
            return introductionJson;
            
        }
        /// <summary>
        /// 檢查內容結構是否被破壞
        /// </summary>
        /// <param name="introductionJson"></param>
        /// <returns></returns>
        private static bool checkJsonIsVailed(JObject introductionJson)
        {
            if (introductionJson == null ||
                !introductionJson.ContainsKey("Name") || !introductionJson.ContainsKey("HomeTown") || !introductionJson.ContainsKey("BirthDate"))
            {
                return false;
            }
            else if (!Regex.IsMatch(introductionJson["BirthDate"].ToString().Split('-')[0], @"\d") ||
                     !Regex.IsMatch(introductionJson["BirthDate"].ToString().Split('-')[1], @"\d") ||
                     !Regex.IsMatch(introductionJson["BirthDate"].ToString().Split('-')[2], @"\d"))
            {
                return false;
            }
            else if (!File.Exists(imagePath))
            {
                return false;
            }
            return true;
        }
        /// <summary>
        /// 取得自我介紹資訊
        /// </summary>
        /// <param name="introductionJson"></param>
        /// <returns></returns>
        private static IntroductionOBJ getIntroductionJsonStr(JObject introductionJson)
        {
            IntroductionOBJ introductionOBJ = new IntroductionOBJ();
            introductionOBJ.name = introductionJson["Name"].ToString();
            introductionOBJ.homeTown = introductionJson["HomeTown"].ToString();
            introductionOBJ.birthDate = new DateTime(int.Parse(introductionJson["BirthDate"].ToString().Split('-')[0]),
                                                     int.Parse(introductionJson["BirthDate"].ToString().Split('-')[1]),
                                                     int.Parse(introductionJson["BirthDate"].ToString().Split('-')[2]));
            return introductionOBJ;
        }
        /// <summary>
        /// 自我介紹 寫檔流程
        /// </summary>
        /// <param name="introductionOBJ"></param>
        public static void processSave(IntroductionOBJ introductionOBJ)
        {
            prepareWrite();
            writeToJson(introductionOBJ);
            saveImageToFile(introductionOBJ.photo);
        }
        private static void prepareWrite()
        {
            // 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
            if (!Directory.Exists(dirPath))
            {
                Directory.CreateDirectory(dirPath);
            }
            // 檔案不存在,產生個人資訊的檔案
            if (!File.Exists(jsonPath))
            {
                File.Create(jsonPath).Close();
            }
        }
        private static void writeToJson(IntroductionOBJ introductionOBJ)
        {
            JObject introductionJson = new JObject();
            introductionJson.Add("Name", introductionOBJ.name);
            introductionJson.Add("HomeTown", introductionOBJ.homeTown);
            introductionJson.Add("BirthDate", string.Format("{0}-{1}-{2}", introductionOBJ.birthDate.Year,
                                                                           introductionOBJ.birthDate.Month,
                                                                           introductionOBJ.birthDate.Day));
            File.WriteAllText(jsonPath, JsonConvert.SerializeObject(introductionJson));
        }
        private static void saveImageToFile(byte[] imageByte)
        {
            Image newImage = PhotoHelper.bytesToImage(imageByte);
            newImage.Save(imagePath, ImageFormat.Jpeg);
        }
    }
}
如果要保存自我介紹的資訊,可透過先前封裝的「IntroductionOBJ」,從UI 傳到 處理讀寫檔的
以下分別是「自我介紹主畫面載入後(IntroductionForm_Load)」重構前後的程式碼
/// <summary>
/// 顯示邀請自我介紹的文字
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void IntroductionForm_Load(object sender, EventArgs e)
{
    loadSaveIntroduction();
    MessageBox.Show(LanguageResources.FormStart);
}
/// <summary>
/// 將自我介紹的資訊顯示到畫面上
/// </summary>
private void loadSaveIntroduction()
{
    string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
    string filePath = Path.Combine(dirPath, "Introduction.json");
    string imagePath = Path.Combine(dirPath, "Photo.jpeg");
    // 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
    if (!Directory.Exists(dirPath) || !File.Exists(filePath) || !File.Exists(imagePath))
    {
        return;
    }
    // 從JSON檔讀取先前保存的個人資訊
    string introductionJsonStr = File.ReadAllText(filePath);
    JObject introductionJson = (JObject)JsonConvert.DeserializeObject(introductionJsonStr);
    //讀完資料發現資料空白、結構有缺漏、結構被破壞,不要讀取
    if (introductionJson == null || 
        !introductionJson.ContainsKey("Name") || !introductionJson.ContainsKey("HomeTown") || !introductionJson.ContainsKey("BirthDate"))
    {
        return;
    }
    else
    {
        nameTextBox.Text = introductionJson["Name"].ToString();
        homeTownTextBox.Text = introductionJson["HomeTown"].ToString();
        birthdate_YearBox.Text = introductionJson["BirthDate"].ToString().Split('-')[0];
        birthdate_MonthBox.Text = introductionJson["BirthDate"].ToString().Split('-')[1];
        birthdate_DayBox.Text = introductionJson["BirthDate"].ToString().Split('-')[2];
    }
    photoBox.Image = Image.FromFile(imagePath);
}
/// <summary>
/// 顯示邀請自我介紹的文字
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void IntroductionForm_Load(object sender, EventArgs e)
{
    loadSaveIntroduction();
    MessageBox.Show(LanguageResources.FormStart);
}
/// <summary>
/// 將自我介紹的資訊顯示到畫面上
/// </summary>
private void loadSaveIntroduction()
{
    IntroductionOBJ introductionOBJ = FileHelper.processRead();
    nameTextBox.Text = introductionOBJ.name;
    homeTownTextBox.Text = introductionOBJ.homeTown;
    birthdate_YearBox.Text = introductionOBJ.birthDate.Year.ToString();
    birthdate_MonthBox.Text = introductionOBJ.birthDate.Month.ToString();
    birthdate_DayBox.Text = introductionOBJ.birthDate.Day.ToString();
    if (introductionOBJ.photo != null)
        photoBox.Image = PhotoHelper.bytesToImage(introductionOBJ.photo);
}
以下是「保存個人資訊」 重構前後的程式碼
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
    try
    {
        IntroductionOBJ introductionOBJ = getIntroductionFromView();
        
        JObject introductionJson = new JObject();
        introductionJson.Add("Name", introductionOBJ.name);
        introductionJson.Add("HomeTown", introductionOBJ.homeTown);
        introductionJson.Add("BirthDate", 
        string.Format("{0}-{1}-{2}", introductionOBJ.birthDate.Year,                                                          
                                     introductionOBJ.birthDate.Month,                                                           
                                     introductionOBJ.birthDate.Day));
        string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
        string jsonPath = Path.Combine(dirPath, "Introduction.json");
        string imagePath = Path.Combine(dirPath, "Photo.jpeg");
        // 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);
        }
        // 檔案不存在,產生個人資訊的檔案
        if (!File.Exists(jsonPath))
        {
            File.Create(jsonPath).Close();
        }
        // 保存個人資訊到JSON
        File.WriteAllText(jsonPath, JsonConvert.SerializeObject(introductionJson));
        // 保存個人大頭貼到Jpeg 圖片
        photoBox.Image.Save(imagePath, ImageFormat.Jpeg);
    }
    catch (Exception error)
    {
        MessageBox.Show(error.Message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
    try
    {
        IntroductionOBJ introductionOBJ = getIntroductionFromView();
        FileHelper.processSave(introductionOBJ);
        MessageBox.Show("保存完成!", "訊息", MessageBoxButtons.OK, MessageBoxIcon.Information);
    }
    catch (Exception error)
    {
        MessageBox.Show(error.Message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}
如果將PictureBox的Image存到IntroductionOBJ,存檔的時候會發生「在 GDI+ 中發生泛型錯誤」,原因是PictureBox透過Image.FromFile佔用了圖片檔,所以我們改以Byte[]型態儲存圖片。
Byte、Image跟Stream的轉換處理,我們另外抽離到新的類別檔案 PhotoHelper.cs introductionOBJ.photo = Image.FromFile(imagePath);
// PhotoHelper.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
namespace IT_Day01
{
    public class PhotoHelper
    {
        /// <summary>
        /// 圖片轉Bytes
        /// </summary>
        /// <param name="img"></param>
        /// <returns></returns>
        public static byte[] ImageToBytes(Image img)
        {
            MemoryStream memoryStream = new MemoryStream();
            img.Save(memoryStream, ImageFormat.Jpeg);
            return memoryStream.ToArray();
        }
        /// <summary>
        /// Bytes轉圖片
        /// </summary>
        /// <param name="imgBytes"></param>
        /// <returns></returns>
        public static Image bytesToImage(byte[] imgBytes)
        {
            MemoryStream memoryStream = new MemoryStream(imgBytes);
            return Image.FromStream(memoryStream);
        }
        public static MemoryStream readImageStreamFromFile(string imagePath)
        {
            MemoryStream memoryStream = new MemoryStream();
            Image imageFile = Image.FromFile(imagePath);
            imageFile.Save(memoryStream, ImageFormat.Jpeg);
            
            // 釋放資源,避免圖片檔案被占用
            imageFile.Dispose();
            return memoryStream;
        }
    }
}
設定圖片到PictureBox上面
photoBox.Image = PhotoHelper.bytesToImage(introductionOBJ.photo);
將PictureBox的圖片以Byte[]型態存到IntroductionOBJ
introductionOBJ.photo = PhotoHelper.ImageToBytes(photoBox.Image);
這次的重構過程沒有用到太深的物件導向觀念(繼承、抽象、多型),主要用了「封裝」和「靜態方法」抽離UI和非UI的程式邏輯。
雖然對於一個小程式的程式碼量而言,重構的效益相對沒有像大系統一樣明顯,但是如果是一個真正的資訊系統,由於包含非常多模組,每個模組至少有上千行的程式碼,如果UI(View)、資料處理邏輯(Controller)、資料實體(Entity)全部混在一起,一旦程式碼出問題,需要付出非常大的維護成本。
在幫「自我介紹程式」畫下句點之前,我會分享一些自身程式教、學相關的非技術議題,以及這支專案的相關衍伸議題和應用。